Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / View Controllers / UserProfileViewController.swift @ 9a71f7ed

History | View | Annotate | Download (19.7 KB)

1
//
2
//  OMEMODeviceVerificationViewController.swift
3
//  ChatSecure
4
//
5
//  Created by Chris Ballinger on 10/13/16.
6
//  Copyright © 2016 Chris Ballinger. All rights reserved.
7
//
8

    
9
import UIKit
10
import XLForm
11
import YapDatabase
12
import OTRAssets
13

    
14
open class UserProfileViewController: XLFormViewController {
15
    
16
    @objc open var completionBlock: (()->Void)?
17
    
18
    // Crypto Chooser row tags
19
    open static let DefaultRowTag = "DefaultRowTag"
20
    open static let PlaintextRowTag = "PlaintextRowTag"
21
    open static let OTRRowTag = "OTRRowTag"
22
    open static let OMEMORowTag = "OMEMORowTag"
23
    open static let ShowAdvancedCryptoSettingsTag = "ShowAdvancedCryptoSettingsTag"
24
    
25
    open let accountKey:String
26
    open var connection: YapDatabaseConnection
27
    
28
    lazy var signalCoordinator:OTROMEMOSignalCoordinator? = {
29
        var account:OTRAccount? = nil
30
        self.connection.read { (transaction) in
31
            account = OTRAccount.fetchObject(withUniqueID: self.accountKey, transaction: transaction)
32
        }
33
        
34
        guard let acct = account else {
35
            return nil
36
        }
37
        
38
        guard let xmpp = OTRProtocolManager.sharedInstance().protocol(for: acct) as? OTRXMPPManager else {
39
            return nil
40
        }
41
        return xmpp.omemoSignalCoordinator
42
    }()
43
    
44
    @objc public init(accountKey:String, connection: YapDatabaseConnection, form: XLFormDescriptor) {
45
        self.accountKey = accountKey
46
        self.connection = connection
47
        super.init(nibName: nil, bundle: nil)
48
        
49
        self.form = form
50
    }
51
    
52
    required public init!(coder aDecoder: NSCoder!) {
53
        fatalError("init(coder:) has not been implemented")
54
    }
55

    
56
    open override func viewDidLoad() {
57
        // gotta register cell before super
58
        OMEMODeviceFingerprintCell.registerCellClass(OMEMODeviceFingerprintCell.defaultRowDescriptorType())
59
        UserInfoProfileCell.registerCellClass(UserInfoProfileCell.defaultRowDescriptorType())
60

    
61
        super.viewDidLoad()
62
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed(_:)))
63
        self.tableView.allowsMultipleSelectionDuringEditing = false
64
        
65
        // Overriding superclass behaviour. This prevents the red icon on left of cell for deletion. Just want swipe to delete on device/fingerprint.
66
        self.tableView.setEditing(false, animated: false)
67
    }
68

    
69
    open override func didReceiveMemoryWarning() {
70
        super.didReceiveMemoryWarning()
71
        // Dispose of any resources that can be recreated.
72
    }
73
    
74
    @objc open func doneButtonPressed(_ sender: AnyObject?) {
75
        var devicesToSave: [OTROMEMODevice] = []
76
        var otrFingerprintsToSave: [OTRFingerprint] = []
77
        for (_, value) in form.formValues() {
78
            switch value {
79
            case let device as OTROMEMODevice:
80
                devicesToSave.append(device)
81
            case let fingerprint as OTRFingerprint:
82
                otrFingerprintsToSave.append(fingerprint)
83
            default:
84
                break
85
            }
86
        }
87
        OTRDatabaseManager.sharedInstance().readWriteDatabaseConnection?.asyncReadWrite({ (t: YapDatabaseReadWriteTransaction) in
88
            for viewedDevice in devicesToSave {
89
                if var device = t.object(forKey: viewedDevice.uniqueId, inCollection: OTROMEMODevice.collection) as? OTROMEMODevice {
90
                    device = device.copy() as! OTROMEMODevice
91
                    device.trustLevel = viewedDevice.trustLevel
92
                    
93
                    if (device.trustLevel == .trustedUser && device.isExpired()) {
94
                        device.lastSeenDate = viewedDevice.lastSeenDate
95
                    }
96
                    
97
                    device.save(with: t)
98
                }
99
            }
100
        })
101
        
102
        otrFingerprintsToSave.forEach { (fingerprint) in
103
            OTRProtocolManager.sharedInstance().encryptionManager.save(fingerprint)
104
        }
105
        if let completion = self.completionBlock {
106
            completion()
107
        }
108
        dismiss(animated: true, completion: nil)
109
    }
110
    
111
    fileprivate func isAbleToDeleteCellAtIndexPath(_ indexPath:IndexPath) -> Bool {
112
        if let rowDescriptor = self.form.formRow(atIndex: indexPath) {
113
            
114
            switch rowDescriptor.value {
115
            case let device as OTROMEMODevice:
116
                if let myBundle = self.signalCoordinator?.fetchMyBundle() {
117
                    // This is only used to compare so we don't allow delete UI on our device
118
                    let thisDeviceYapKey = OTROMEMODevice.yapKey(withDeviceId: NSNumber(value: myBundle.deviceId as UInt32), parentKey: self.accountKey, parentCollection: OTRAccount.collection)
119
                    if device.uniqueId != thisDeviceYapKey {
120
                        return true
121
                    }
122
                }
123
            case let fingerprint as OTRFingerprint:
124
                if (fingerprint.accountName != fingerprint.username) {
125
                    return true
126
                }
127
            default:
128
                break
129
            }
130
        }
131
        return false
132
    }
133
    
134
    fileprivate func performEdit(_ action:UITableViewCellEditingStyle, indexPath:IndexPath) {
135
        if ( action == .delete ) {
136
            if let rowDescriptor = self.form.formRow(atIndex: indexPath) {
137
                rowDescriptor.sectionDescriptor.removeFormRow(rowDescriptor)
138
                switch rowDescriptor.value {
139
                case let device as OTROMEMODevice:
140
                    
141
                    self.signalCoordinator?.removeDevice([device], completion: { (success) in
142
                        
143
                    })
144
                    break
145
                case let fingerprint as OTRFingerprint:
146
                    do {
147
                        try OTRProtocolManager.sharedInstance().encryptionManager.otrKit.delete(fingerprint)
148
                    } catch {
149
                        
150
                    }
151
                    break
152
                default:
153
                    break
154
                }
155
            }
156
        }
157
    }
158
    
159
    open static func cryptoChooserRows(_ buddy: OTRBuddy, connection: YapDatabaseConnection) -> [XLFormRowDescriptor] {
160
        
161
        let bestAvailableRow = XLFormRowDescriptor(tag: DefaultRowTag, rowType: XLFormRowDescriptorTypeBooleanCheck, title: Best_Available())
162
        let plaintextOnlyRow = XLFormRowDescriptor(tag: PlaintextRowTag, rowType: XLFormRowDescriptorTypeBooleanCheck, title: Plaintext_Only())
163
        let plaintextOtrRow = XLFormRowDescriptor(tag: PlaintextRowTag, rowType: XLFormRowDescriptorTypeBooleanCheck, title: Plaintext_Opportunistic_OTR())
164
        let otrRow = XLFormRowDescriptor(tag: OTRRowTag, rowType: XLFormRowDescriptorTypeBooleanCheck, title: "OTR")
165
        let omemoRow = XLFormRowDescriptor(tag: OMEMORowTag, rowType: XLFormRowDescriptorTypeBooleanCheck, title: "OMEMO")
166
        
167
        var hasDevices = false
168
        
169
        connection.read { (transaction: YapDatabaseReadTransaction) in
170
            if OTROMEMODevice.allDevices(forParentKey: buddy.uniqueId, collection: type(of: buddy).collection, transaction: transaction).count > 0 {
171
                hasDevices = true
172
            }
173
        }
174
        
175
        if (!hasDevices) {
176
            omemoRow.disabled = NSNumber(value: true as Bool)
177
        }
178
        
179
        let trueValue = NSNumber(value: true as Bool)
180
        switch buddy.preferredSecurity {
181
        case .plaintextOnly:
182
            plaintextOnlyRow.value = trueValue
183
            break
184
        case .bestAvailable:
185
            bestAvailableRow.value = trueValue
186
            break
187
        case .OTR:
188
            otrRow.value = trueValue
189
            break
190
        case .OMEMO:
191
            omemoRow.value = trueValue
192
            break
193
        case .omemOandOTR:
194
            omemoRow.value = trueValue
195
            break
196
        case .plaintextWithOTR:
197
            plaintextOtrRow.value = trueValue
198
        }
199
        
200
        let formRows = [bestAvailableRow, plaintextOnlyRow, plaintextOtrRow, otrRow, omemoRow]
201
        
202
        var currentRow: XLFormRowDescriptor? = nil
203
        var rowsToDeselect: NSMutableSet = NSMutableSet()
204
        let onChangeBlock = { (oldValue: Any?, newValue: Any?, rowDescriptor: XLFormRowDescriptor) in
205
            // Prevent infinite loops
206
            // Allow deselection
207
            if rowsToDeselect.count > 0 {
208
                rowsToDeselect.remove(rowDescriptor)
209
                return
210
            }
211
            if currentRow != nil {
212
                return
213
            }
214
            currentRow = rowDescriptor
215
            
216
            // Don't allow user to unselect a true value
217
            if (newValue as AnyObject?)?.boolValue == false {
218
                rowDescriptor.value = NSNumber(value: true as Bool)
219
                currentRow = nil
220
                return
221
            }
222
            
223
            // Deselect other rows
224
            rowsToDeselect = NSMutableSet(array: formRows.filter({ $0 != rowDescriptor }))
225
            for row in rowsToDeselect {
226
                guard let row = row as? XLFormRowDescriptor else {
227
                    continue
228
                }
229
                let newValue = NSNumber(value: false as Bool)
230
                row.value = newValue
231
                // Wow that's janky
232
                (row.sectionDescriptor.formDescriptor.delegate as! XLFormViewControllerDelegate).reloadFormRow!(row)
233
            }
234
            
235
            var preferredSecurity: OTRSessionSecurity = .bestAvailable
236
            if (plaintextOnlyRow.value as AnyObject?)?.boolValue == true {
237
                preferredSecurity = .plaintextOnly
238
            } else if (otrRow.value as AnyObject?)?.boolValue == true {
239
                preferredSecurity = .OTR
240
            } else if (omemoRow.value as AnyObject?)?.boolValue == true {
241
                preferredSecurity = .OMEMO
242
            } else if (bestAvailableRow.value as AnyObject?)?.boolValue == true {
243
                preferredSecurity = .bestAvailable
244
            } else if (plaintextOtrRow.value as AnyObject?)?.boolValue == true {
245
                preferredSecurity = .plaintextWithOTR
246
            }
247
            
248
            OTRDatabaseManager.sharedInstance().readWriteDatabaseConnection?.readWrite({ (transaction: YapDatabaseReadWriteTransaction) in
249
                guard var buddy = transaction.object(forKey: buddy.uniqueId, inCollection: type(of: buddy).collection) as? OTRBuddy else {
250
                    return
251
                }
252
                guard let account = buddy.account(with: transaction) else {
253
                    return
254
                }
255
                buddy = buddy.copy() as! OTRBuddy
256
                buddy.preferredSecurity = preferredSecurity
257
                buddy.save(with: transaction)
258
                // Cancel OTR session if plaintext or omemo only
259
                if (preferredSecurity == .plaintextOnly || preferredSecurity == .OMEMO) {
260
                    OTRProtocolManager.sharedInstance().encryptionManager.otrKit.disableEncryption(withUsername: buddy.username, accountName: account.username, protocol: account.protocolTypeString())
261
                }
262
            })
263
            currentRow = nil
264
        }
265
        
266
        for row in formRows {
267
            row.onChangeBlock = onChangeBlock
268
        }
269

    
270
        return formRows
271
    }
272
    
273
//MARK UITableView Delegate overrides
274
    
275
    open override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
276
        if  self.isAbleToDeleteCellAtIndexPath(indexPath) {
277
            return true
278
        }
279
        return false
280
    }
281
    
282
    open override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
283
        if  self.isAbleToDeleteCellAtIndexPath(indexPath) {
284
            return .delete
285
        }
286
        return .none
287
    }
288
    
289
    open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
290
        
291
        self.performEdit(editingStyle, indexPath: indexPath)
292
    }
293
    
294
    
295
    @objc open static func profileFormDescriptorForAccount(_ account: OTRAccount, buddies: [OTRBuddy], connection: YapDatabaseConnection) -> XLFormDescriptor {
296
        let form = XLFormDescriptor(title: Profile_String())
297
        
298
        let yourProfileSection = XLFormSectionDescriptor.formSection(withTitle: Me_String())
299
        let yourProfileRow = XLFormRowDescriptor(tag: account.uniqueId, rowType: UserInfoProfileCell.defaultRowDescriptorType())
300
        yourProfileRow.value = account
301
        yourProfileSection.addFormRow(yourProfileRow)
302
        
303
        guard let xmpp = OTRProtocolManager.sharedInstance().protocol(for: account) as? OTRXMPPManager else {
304
            return form
305
        }
306
        guard let myBundle = xmpp.omemoSignalCoordinator?.fetchMyBundle() else {
307
            return form
308
        }
309
        let thisDevice = OTROMEMODevice(deviceId: NSNumber(value: myBundle.deviceId as UInt32), trustLevel: .trustedUser, parentKey: account.uniqueId, parentCollection: type(of: account).collection, publicIdentityKeyData: myBundle.identityKey, lastSeenDate: Date())
310
        var ourDevices: [OTROMEMODevice] = []
311
        connection.read { (transaction: YapDatabaseReadTransaction) in
312
            ourDevices = OTROMEMODevice.allDevices(forParentKey: account.uniqueId, collection: type(of: account).collection, transaction: transaction)
313
        }
314

    
315
        
316
        let ourFilteredDevices = ourDevices.filter({ (device: OTROMEMODevice) -> Bool in
317
            return device.uniqueId != thisDevice.uniqueId
318
        })
319
        
320
        // TODO - Sort ourDevices and theirDevices by lastSeen
321
        
322
        let addDevicesToSection: ([OTROMEMODevice], XLFormSectionDescriptor) -> Void = { devices, section in
323
            for device in devices {
324
                guard let _ = device.publicIdentityKeyData else {
325
                    continue
326
                }
327
                let row = XLFormRowDescriptor(tag: device.uniqueId, rowType: OMEMODeviceFingerprintCell.defaultRowDescriptorType())
328
                row.value = device.copy()
329
                
330
                // Don't allow editing of your own device
331
                if device.uniqueId == thisDevice.uniqueId {
332
                    row.disabled = true
333
                }
334
                
335
                section.addFormRow(row)
336
            }
337
        }
338
        
339
        let otrKit = OTRProtocolManager.sharedInstance().encryptionManager.otrKit
340
        let allFingerprints = otrKit.allFingerprints()
341
        let myFingerprint = otrKit.fingerprint(forAccountName: account.username, protocol: account.protocolTypeString())
342
        let addFingerprintsToSection: ([OTRFingerprint], XLFormSectionDescriptor) -> Void = { fingerprints, section in
343
            for fingerprint in fingerprints {
344
                let row = XLFormRowDescriptor(tag: (fingerprint.fingerprint as NSData).otr_hexString(), rowType: OMEMODeviceFingerprintCell.defaultRowDescriptorType())
345
                if let myFingerprint = myFingerprint {
346
                    if (fingerprint === myFingerprint) {
347
                        // We implicitly trust ourselves with OTR
348
                        row.disabled = true
349
                    } else {
350
                        row.disabled = false
351
                    }
352
                }
353
                
354
                row.value = fingerprint
355
                
356
                section.addFormRow(row)
357
            }
358
        }
359
        
360
        var allMyDevices: [OTROMEMODevice] = []
361
        allMyDevices.append(thisDevice)
362
        allMyDevices.append(contentsOf: ourFilteredDevices)
363
        addDevicesToSection(allMyDevices, yourProfileSection)
364
        
365
        var theirSections: [XLFormSectionDescriptor] = []
366

    
367
        if let myFingerprint = myFingerprint {
368
            addFingerprintsToSection([myFingerprint], yourProfileSection)
369
        }
370
        
371
        // Add section for each buddy's device
372
        for buddy in buddies {
373
            let theirSection = XLFormSectionDescriptor.formSection(withTitle: buddy.username)
374

    
375
            let buddyRow = XLFormRowDescriptor(tag: buddy.uniqueId, rowType: UserInfoProfileCell.defaultRowDescriptorType())
376
            buddyRow.value = buddy
377
            theirSection.addFormRow(buddyRow)
378
            var theirDevices: [OTROMEMODevice] = []
379
            connection.read({ (transaction: YapDatabaseReadTransaction) in
380
                theirDevices = OTROMEMODevice.allDevices(forParentKey: buddy.uniqueId, collection: type(of: buddy).collection, transaction: transaction)
381
            })
382
            let theirFingerprints = allFingerprints.filter({ (fingerprint: OTRFingerprint) -> Bool in
383
                return fingerprint.username == buddy.username &&
384
                fingerprint.accountName == account.username
385
            })
386

    
387
            addDevicesToSection(theirDevices, theirSection)
388
            addFingerprintsToSection(theirFingerprints, theirSection)
389
            theirSections.append(theirSection)
390
        }
391
 
392
        
393
        var sectionsToAdd: [XLFormSectionDescriptor] = []
394
        sectionsToAdd.append(contentsOf: theirSections)
395
        
396
        // cryptoChooserRows is only meaningful for 1:1 conversations at the moment
397
        if buddies.count == 1 {
398
            let buddy = buddies.first!
399
            let cryptoSection = XLFormSectionDescriptor.formSection(withTitle: Advanced_Encryption_Settings())
400
            cryptoSection.footerTitle = Advanced_Crypto_Warning()
401
            let showAdvancedSwitch = XLFormRowDescriptor.init(tag: self.ShowAdvancedCryptoSettingsTag, rowType: XLFormRowDescriptorTypeBooleanSwitch, title: Show_Advanced_Encryption_Settings())
402
            showAdvancedSwitch.value = NSNumber(value: false as Bool)
403
            let cryptoChooser = cryptoChooserRows(buddy, connection: connection)
404
            for row in cryptoChooser {
405
                cryptoSection.addFormRow(row)
406
            }
407
            cryptoSection.hidden = "$\(ShowAdvancedCryptoSettingsTag)==0"
408
            let buddySection = theirSections.first!
409
            buddySection.addFormRow(showAdvancedSwitch)
410
            sectionsToAdd.append(cryptoSection)
411
        }
412
        
413
        sectionsToAdd.append(yourProfileSection)
414
    
415
        for section in sectionsToAdd {
416
            if section.formRows.count > 0 {
417
                form.addFormSection(section)
418
            }
419
        }
420
        
421
        return form
422
    }
423
    
424
    // MARK: - UITableViewDelegate
425
    
426
    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
427
        super.tableView(tableView, didSelectRowAt: indexPath)
428
        tableView.deselectRow(at: indexPath, animated: true)
429
        guard let cell = self.tableView(tableView, cellForRowAt: indexPath) as? OMEMODeviceFingerprintCell else {
430
            return
431
        }
432
        var fingerprint = ""
433
        var username = ""
434
        var cryptoType = ""
435
        if let device = cell.rowDescriptor.value as? OTROMEMODevice {
436
            cryptoType = "OMEMO"
437
            fingerprint = device.humanReadableFingerprint
438
            self.connection.read({ (transaction) in
439
                if let buddy = transaction.object(forKey: device.parentKey, inCollection: device.parentCollection) as? OTRBuddy {
440
                    username = buddy.username
441
                }
442
            })
443
        }
444
        if let otrFingerprint = cell.rowDescriptor.value as? OTRFingerprint {
445
            cryptoType = "OTR"
446
            fingerprint = (otrFingerprint.fingerprint as NSData).humanReadableFingerprint()
447
            username = otrFingerprint.username
448
        }
449
        if fingerprint.count == 0 || username.count == 0 || cryptoType.count == 0 {
450
            return
451
        }
452
        let stringToShare = "\(username): \(cryptoType) \(fingerprint)"
453
        let activityViewController = UIActivityViewController(activityItems: [stringToShare], applicationActivities: nil)
454
        if let ppc = activityViewController.popoverPresentationController {
455
            ppc.sourceView = cell
456
            ppc.sourceRect = cell.frame
457
        }
458
        present(activityViewController, animated: true, completion: nil)
459
    }
460

    
461
}